/*
 * @(#) $Header$
 *
 * Copyright (C) 2006  Daniel Léonard
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 */

package ca.forklabs.baselib.io;

import java.io.FilterReader;
import java.io.Reader;
import java.io.Writer;
import ca.forklabs.baselib.io.NullWriter;

import java.io.IOException;

/**
 * Class {@code TappedReader} is a reader that is tapped. All the characters
 * that are read and skipped are written to a spy writer. The spy writer is
 * never closed.
 *
 * @author   <a href="mailto:forklabs at gmail.com?subject=ca.forklabs.baselib.io.TappedReader">Daniel Léonard</a>
 * @version $Revision$
 */
public class TappedReader extends FilterReader {

//---------------------------
// Instance variable
//---------------------------

   /** The tap. */
   private Writer tap;


//---------------------------
// Constructors
//---------------------------

   /**
    * Constructor. No tap is set.
    * @param   reader   the source.
    */
   public TappedReader(Reader reader) {
      this(reader, null);
      }

   /**
    * Constructor.
    * @param   reader    the source.
    * @param   tap   the tap.
    */
   public TappedReader(Reader reader, Writer tap) {
      super(reader);
      this.setTap(tap);
      }


//---------------------------
// Accessors / mutators
//---------------------------

   /**
    * Gets the tap.
    * @return   the tap.
    */
   public Writer getTap() {
      return this.tap;
      }

   /**
    * Changes the tap.
    * @param   tap   the new tap.
    */
   public void setTap(Writer tap) {
      Writer spy = tap;
      if (null == spy) {
         spy = new NullWriter();
         }
      this.tap = spy;
      }


//---------------------------
// Instance methods
//---------------------------

   /**
    * Writes the given character into the tap.
    * @param   c   the character to write.
    * @exception   IOException   if anything goes wrong with I/O.
    */
   @SuppressWarnings("resource")
   protected void tap(int c) throws IOException {
      Writer out = this.getTap();
      out.write(c);
      out.flush();
      }

   /**
    * Writes the given characters into the tap.
    * @param   characters   the bytes.
    * @param   off   the start offset of the data.
    * @param   len   the number of bytes to write.
    * @exception   IOException   if anything goes wrong with I/O.
    */
   @SuppressWarnings("resource")
   protected void tap(char[] characters, int off, int len) throws IOException {
      Writer out = this.getTap();
      out.write(characters, off, len);
      out.flush();
      }

   /**
    * Gets an error message saying that it is impossible to skip a negative
    * number of characters.
    * @param   bad_skip   the negative number of bytes.
    * @return   the formatted error message.
    */
   @SuppressWarnings("boxing")
   protected String getNegativeSkipErrorMessage(long bad_skip) {
      String key = Resources.INPUT_STREAM_NEGATIVE_SKIP;
      String message = Resources.getLocalizedString(key, bad_skip);
      return message;
      }


//---------------------------
// Overridden methods from java.io.FilterReader
//---------------------------

   /**
    * Reads one character and writes it into the tap.
    * @return   the character read.
    * @exception   IOException   if anything goes wrong with I/O.
    */
   @Override
   public int read() throws IOException {
      int c = super.read();
      if (-1 != c) {
         this.tap(c);
         }
      return c;
      }

   /**
    * Reads as many characters as possible and write them into the tap.
    * @param   characters   the buffer into which the data is read.
    * @param   off   the start offset of the data.
    * @param   len   the maximum number of bytes read.
    * @return  the number of characters read.
    * @exception   IOException   if anything goes wrong with I/O.
    */
   @Override
   public int read(char[] characters, int off, int len) throws IOException {
      int read = super.read(characters, off, len);
      if (-1 != read) {
         this.tap(characters, off, read);
         }
      return read;
      }

   /**
    * Skips the specified number of characters and write them to the tapper.
    * @param  n  the number of characters to skip.
    * @return  the number of character skipped.
    * @exception   IOException   if anything goes wrong with I/O.
    */
   @Override
   public long skip(long n) throws IOException {
      if (n < 0L) {
         String message = this.getNegativeSkipErrorMessage(n);
         throw new IllegalArgumentException(message);
         }
      else if (n == 0L) {
         return n;
         }

      long block_size = 1024L;
      char[] characters = new char[(int) block_size];
      long total = 0;

      int len = 0;
      int read = 0;
      while (read == len) {
         len = (int) Math.min(block_size, n - total);
         read = this.read(characters, 0, len);
         total += read;
         }

      return total;
      }

   }
